/**
 * Created: May 3, 2008
 */
package com.matthicks.delegates;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * MethodDelegates reference a specific instance and method name
 * that receives arguments either at invocation time or if pre-set
 * before execution.
 * 
 * @author Matt Hicks
 */
public class MethodDelegate implements Delegate {
	private Object obj;
	private Method method;
	private Object[] arguments;
	
	public MethodDelegate(Object obj, Method method) {
		this.obj = obj;
		this.method = method;
		
		if (method == null) {
			throw new NullPointerException("Method cannot be null");
		} else if (((method.getModifiers() & Modifier.STATIC) != Modifier.STATIC) && (obj == null)) {
			throw new NullPointerException("Cannot pass null Object for non-static method: " + method.getName());
		}
	}
	
	public void setArguments(Object ... arguments) {
		this.arguments = arguments;
	}
	
	public Object invoke(Object ... args) {
		try {
			Object[] tmp = new Object[method.getParameterTypes().length];
			int pos = 0;
			if ((arguments != null) && (pos < tmp.length)) {
				for (int i = 0; i < arguments.length; i++) {
					if (pos >= tmp.length) {
						break;
					}
					tmp[pos++] = arguments[i];
				}
			}
			if (pos < tmp.length) {
				for (int i = 0; i < args.length; i++) {
					if (pos >= tmp.length) {
						break;
					}
					tmp[pos++] = args[i];
				}
			}
    		return method.invoke(obj, tmp);
		} catch(IllegalArgumentException exc) {
			System.err.println("Wrong number of arguments, Received: " + args.length + ", Expected: " + args.length);
			exc.printStackTrace();
		} catch(Exception exc) {
			throw new RuntimeException("Exception during invocation of MethodDelegate (" + obj.getClass().getCanonicalName() + "." + method.getName() + ")", exc);
		}
		return null;
	}
	
	private static final MethodDelegate create(Class<?> c, Object obj, String methodName, boolean matchReverse, Class<?> ... args) {
		boolean includeStatic = obj == null;
		Method[] methods = ClassUtilities.getMethods(c, includeStatic, true);
		
		// Check for method that matches signature
		methodLoop: for (Method m : methods) {
			if (m.getName().equals(methodName)) {
				if (m.getParameterTypes().length == args.length) {
					for (int i = 0; i < args.length; i++) {
						Class<?> type = ClassUtilities.updateClass(m.getParameterTypes()[i]);
						if (!type.isAssignableFrom(args[i])) {
							if (matchReverse) {
								if (!args[i].isAssignableFrom(type)) {
//									System.err.println("Not Assignable(" + c + "." + m.getName() + "): " + args[i] + " - " + m.getParameterTypes()[i] + " - " + args[i].isAssignableFrom(type));
									continue methodLoop;
								}
							} else {
//    							System.err.println("Not Assignable(" + c + "." + m.getName() + "): " + args[i] + " - " + m.getParameterTypes()[i] + " - " + args[i].isAssignableFrom(type));
    							continue methodLoop;
							}
						}
					}
					return new MethodDelegate(obj, m);
				}
			}
		}
		
		StringBuilder builder = new StringBuilder(); {
			for (Class<?> cl : args) {
				builder.append(cl + " ");
			}
		}
		throw new RuntimeException("Unable to find Method: " + c.getCanonicalName() + "." + methodName + " - " + builder);
	}

	private static final MethodDelegate createWithArgs(Class<?> c, Object obj, String methodName, boolean matchReverse, Object ... args) {
		Class<?>[] classes = new Class<?>[args.length];
		for (int i = 0; i < classes.length; i++) {
			classes[i] = args[i].getClass();
		}
		MethodDelegate delegate = create(c, obj, methodName, matchReverse, classes);
		
		if (delegate != null) {
    		delegate.setArguments(args);
    		return delegate;
		}
		throw new RuntimeException("Unable to find Method: " + c.getCanonicalName() + "." + methodName);
	}
	
	public static final MethodDelegate create(Object obj, String methodName, boolean matchReverse, Class<?> ... args) {
		return create(obj.getClass(), obj, methodName, matchReverse, args);
	}
	
	public static final MethodDelegate create(Object obj, String methodName, Class<?> ... args) {
		return create(obj, methodName, false, args);
	}
	
	public static final MethodDelegate createWithArgs(Object obj, String methodName, Object ... args) {
		return createWithArgs(obj.getClass(), obj, methodName, false, args);
	}
	
	public static final MethodDelegate create(Class<?> c, String methodName, boolean matchReverse, Class<?> ... args) {
		return create(c, null, methodName, matchReverse, args);
	}
	
	public static final MethodDelegate create(Class<?> c, String methodName, Class<?> ... args) {
		return create(c, methodName, false, args);
	}
	
	public static final MethodDelegate createWithArgs(Class<?> c, String methodName, Object ... args) {
		return createWithArgs(c, null, methodName, false, args);
	}
}